// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;

namespace Microsoft.Extensions.Logging.Generators
{
    public partial class LoggerMessageGenerator
    {
        internal sealed class Emitter
        {
            // The maximum arity of LoggerMessage.Define.
            private const int MaxLoggerMessageDefineArguments = 6;
            private const int DefaultStringBuilderCapacity = 1024;

            private const string GeneratedTypeSummary =
                "<summary> " +
                "This API supports the logging infrastructure and is not intended to be used directly from your code. " +
                "It is subject to change in the future. " +
                "</summary>";
            private static readonly string s_generatedCodeAttribute =
                $"global::System.CodeDom.Compiler.GeneratedCodeAttribute(" +
                $"\"{typeof(Emitter).Assembly.GetName().Name}\", " +
                $"\"{typeof(Emitter).Assembly.GetName().Version}\")";
            private const string EditorBrowsableAttribute =
                "global::System.ComponentModel.EditorBrowsableAttribute(" +
                "global::System.ComponentModel.EditorBrowsableState.Never)";
            private readonly StringBuilder _builder = new StringBuilder(DefaultStringBuilderCapacity);
            private bool _needEnumerationHelper;

            public string Emit(IReadOnlyList<LoggerClass> logClasses, CancellationToken cancellationToken)
            {
                _builder.Clear();
                _builder.AppendLine("// <auto-generated/>");
                _builder.AppendLine("#nullable enable");

                foreach (LoggerClass lc in logClasses)
                {
                    cancellationToken.ThrowIfCancellationRequested();
                    GenType(lc);
                }

                GenEnumerationHelper();
                return _builder.ToString();
            }

            private static bool UseLoggerMessageDefine(LoggerMethod lm)
            {
                bool result =
                    (lm.TemplateParameters.Count <= MaxLoggerMessageDefineArguments) && // more args than LoggerMessage.Define can handle
                    (lm.Level != null) &&                                               // dynamic log level, which LoggerMessage.Define can't handle
                    (lm.TemplateList.Count == lm.TemplateParameters.Count);             // mismatch in template to args, which LoggerMessage.Define can't handle

                if (result)
                {
                    // make sure the order of the templates matches the order of the logging method parameter
                    for (int i = 0; i < lm.TemplateList.Count; i++)
                    {
                        ReadOnlySpan<char> template = RemoveSpecialSymbol(lm.TemplateList[i].AsSpan());
                        ReadOnlySpan<char> parameter = RemoveSpecialSymbol(lm.TemplateParameters[i].CodeName.AsSpan());

                        if (!template.Equals(parameter, StringComparison.OrdinalIgnoreCase))
                        {
                            // order doesn't match, can't use LoggerMessage.Define
                            return false;
                        }
                    }
                }

                return result;
            }

            private void GenType(LoggerClass lc)
            {
                string nestedIndentation = "";
                if (!string.IsNullOrWhiteSpace(lc.Namespace))
                {
                    _builder.Append($@"
namespace {lc.Namespace}
{{");
                }

                LoggerClass parent = lc.ParentClass;
                var parentClasses = new List<string>();
                // loop until you find top level nested class
                while (parent != null)
                {
                    parentClasses.Add($"partial {parent.Keyword} {parent.Name}");
                    parent = parent.ParentClass;
                }

                // write down top level nested class first
                for (int i = parentClasses.Count - 1; i >= 0; i--)
                {
                    _builder.Append($@"
    {nestedIndentation}{parentClasses[i]}
    {nestedIndentation}{{");
                    nestedIndentation += "    ";
                }

                _builder.Append($@"
    {nestedIndentation}partial {lc.Keyword} {lc.Name}
    {nestedIndentation}{{");

                foreach (LoggerMethod lm in lc.Methods)
                {
                    if (!UseLoggerMessageDefine(lm))
                    {
                        GenStruct(lm, nestedIndentation);
                    }

                    GenLogMethod(lm, nestedIndentation);
                }

                _builder.Append($@"
    {nestedIndentation}}}");

                parent = lc.ParentClass;
                while (parent != null)
                {
                    nestedIndentation = new string(' ', nestedIndentation.Length - 4);
                    _builder.Append($@"
    {nestedIndentation}}}");
                    parent = parent.ParentClass;
                }

                if (!string.IsNullOrWhiteSpace(lc.Namespace))
                {
                    _builder.Append($@"
}}");
                }
            }

            private void GenStruct(LoggerMethod lm, string nestedIndentation)
            {
                _builder.AppendLine($@"
        {nestedIndentation}/// {GeneratedTypeSummary}
        {nestedIndentation}[{s_generatedCodeAttribute}]
        {nestedIndentation}[{EditorBrowsableAttribute}]
        {nestedIndentation}private readonly struct __{lm.UniqueName}Struct : global::System.Collections.Generic.IReadOnlyList<global::System.Collections.Generic.KeyValuePair<string, object?>>
        {nestedIndentation}{{");
                GenFields(lm, nestedIndentation);

                if (lm.TemplateParameters.Count > 0)
                {
                    _builder.Append($@"
            {nestedIndentation}public __{lm.UniqueName}Struct(");
                    GenArguments(lm);
                    _builder.Append($@")
            {nestedIndentation}{{");
                    _builder.AppendLine();
                    GenFieldAssignments(lm, nestedIndentation);
                    _builder.Append($@"
            {nestedIndentation}}}
");
                }

                _builder.Append($@"
            {nestedIndentation}public override string ToString()
            {nestedIndentation}{{
");
                GenVariableAssignments(lm, nestedIndentation);
                _builder.Append($@"
                {nestedIndentation}return $""{ConvertEndOfLineAndQuotationCharactersToEscapeForm(lm.Message)}"";
            {nestedIndentation}}}
");
                _builder.Append($@"
            {nestedIndentation}public static readonly global::System.Func<__{lm.UniqueName}Struct, global::System.Exception?, string> Format = (state, ex) => state.ToString();

            {nestedIndentation}public int Count => {lm.TemplateParameters.Count + 1};

            {nestedIndentation}public global::System.Collections.Generic.KeyValuePair<string, object?> this[int index]
            {nestedIndentation}{{
                {nestedIndentation}get => index switch
                {nestedIndentation}{{
");
                GenCases(lm, nestedIndentation);
                _builder.Append($@"
                    {nestedIndentation}_ => throw new global::System.IndexOutOfRangeException(nameof(index)),  // return the same exception LoggerMessage.Define returns in this case
                {nestedIndentation}}};
            }}

            {nestedIndentation}public global::System.Collections.Generic.IEnumerator<global::System.Collections.Generic.KeyValuePair<string, object?>> GetEnumerator()
            {nestedIndentation}{{
                {nestedIndentation}for (int i = 0; i < {lm.TemplateParameters.Count + 1}; i++)
                {nestedIndentation}{{
                    {nestedIndentation}yield return this[i];
                {nestedIndentation}}}
            {nestedIndentation}}}

            {nestedIndentation}global::System.Collections.IEnumerator global::System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
        {nestedIndentation}}}
");
            }

            private void GenFields(LoggerMethod lm, string nestedIndentation)
            {
                foreach (LoggerParameter p in lm.TemplateParameters)
                {
                    _builder.AppendLine($"            {nestedIndentation}private readonly {p.Type} {NormalizeSpecialSymbol(p.CodeName)};");
                }
            }

            private void GenFieldAssignments(LoggerMethod lm, string nestedIndentation)
            {
                foreach (LoggerParameter p in lm.TemplateParameters)
                {
                    _builder.AppendLine($"                {nestedIndentation}this.{NormalizeSpecialSymbol(p.CodeName)} = {p.CodeName};");
                }
            }

            private void GenVariableAssignments(LoggerMethod lm, string nestedIndentation)
            {
                foreach (KeyValuePair<string, string> t in lm.TemplateMap)
                {
                    int index = 0;
                    foreach (LoggerParameter p in lm.TemplateParameters)
                    {
                        ReadOnlySpan<char> template = RemoveSpecialSymbol(t.Key.AsSpan());
                        ReadOnlySpan<char> parameter = RemoveSpecialSymbol(p.Name.AsSpan());
                        if (template.Equals(parameter, StringComparison.OrdinalIgnoreCase))
                        {
                            break;
                        }

                        index++;
                    }

                    // check for an index that's too big, this can happen in some cases of malformed input
                    if (index < lm.TemplateParameters.Count)
                    {
                        if (lm.TemplateParameters[index].IsEnumerable)
                        {
                            _builder.AppendLine($"                {nestedIndentation}var {t.Key} = "
                                + $"global::__LoggerMessageGenerator.Enumerate((global::System.Collections.IEnumerable ?)this.{NormalizeSpecialSymbol(lm.TemplateParameters[index].CodeName)});");

                            _needEnumerationHelper = true;
                        }
                        else
                        {
                            _builder.AppendLine($"                {nestedIndentation}var {t.Key} = this.{NormalizeSpecialSymbol(lm.TemplateParameters[index].CodeName)};");
                        }
                    }
                }
            }

            private void GenCases(LoggerMethod lm, string nestedIndentation)
            {
                int index = 0;
                foreach (LoggerParameter p in lm.TemplateParameters)
                {
                    // this is related to https://github.com/serilog/serilog-extensions-logging/issues/197
                    string name = p.CodeName;
                    if (lm.TemplateMap.TryGetValue(name, out string value))
                    {
                        // take the letter casing from the template
                        name = value;
                    }

                    _builder.AppendLine($"                    {nestedIndentation}{index++} => new global::System.Collections.Generic.KeyValuePair<string, object?>(\"{name}\", this.{NormalizeSpecialSymbol(p.CodeName)}),");
                }

                _builder.AppendLine($"                    {nestedIndentation}{index++} => new global::System.Collections.Generic.KeyValuePair<string, object?>(\"{{OriginalFormat}}\", \"{ConvertEndOfLineAndQuotationCharactersToEscapeForm(lm.Message)}\"),");
            }

            private void GenCallbackArguments(LoggerMethod lm)
            {
                foreach (LoggerParameter p in lm.TemplateParameters)
                {
                    _builder.Append($"{p.CodeName}, ");
                }
            }

            private void GenDefineTypes(LoggerMethod lm, bool brackets)
            {
                if (lm.TemplateParameters.Count == 0)
                {
                    return;
                }
                if (brackets)
                {
                    _builder.Append('<');
                }

                bool firstItem = true;
                foreach (LoggerParameter p in lm.TemplateParameters)
                {
                    if (firstItem)
                    {
                        firstItem = false;
                    }
                    else
                    {
                        _builder.Append(", ");
                    }

                    _builder.Append($"{p.Type}");
                }

                if (brackets)
                {
                    _builder.Append('>');
                }
                else
                {
                    _builder.Append(", ");
                }
            }

            private void GenParameters(LoggerMethod lm)
            {
                bool firstItem = true;
                foreach (LoggerParameter p in lm.AllParameters)
                {
                    if (firstItem)
                    {
                        firstItem = false;
                    }
                    else
                    {
                        _builder.Append(", ");
                    }

                    if (p.Qualifier != null)
                    {
                        _builder.Append($"{p.Qualifier} ");
                    }
                    _builder.Append($"{p.Type} {p.CodeName}");
                }
            }

            private void GenArguments(LoggerMethod lm)
            {
                bool firstItem = true;
                foreach (LoggerParameter p in lm.TemplateParameters)
                {
                    if (firstItem)
                    {
                        firstItem = false;
                    }
                    else
                    {
                        _builder.Append(", ");
                    }

                    _builder.Append($"{p.Type} {p.CodeName}");
                }
            }

            private void GenHolder(LoggerMethod lm)
            {
                string typeName = $"__{lm.UniqueName}Struct";

                _builder.Append($"new {typeName}(");
                foreach (LoggerParameter p in lm.TemplateParameters)
                {
                    if (p != lm.TemplateParameters[0])
                    {
                        _builder.Append(", ");
                    }

                    _builder.Append(p.CodeName);
                }

                _builder.Append(')');
            }

            private void GenLogMethod(LoggerMethod lm, string nestedIndentation)
            {
                string level = GetLogLevel(lm);
                string extension = lm.IsExtensionMethod ? "this " : string.Empty;
                string eventName = string.IsNullOrWhiteSpace(lm.EventName) ? $"nameof({lm.Name})" : $"\"{lm.EventName}\"";
                string exceptionArg = GetException(lm);
                string logger = GetLogger(lm);

                if (UseLoggerMessageDefine(lm))
                {
                    _builder.Append($@"
        {nestedIndentation}[{s_generatedCodeAttribute}]
        {nestedIndentation}private static readonly global::System.Action<global::Microsoft.Extensions.Logging.ILogger, ");

                    GenDefineTypes(lm, brackets: false);

                    _builder.Append($@"global::System.Exception?> __{lm.UniqueName}Callback =
            {nestedIndentation}global::Microsoft.Extensions.Logging.LoggerMessage.Define");

                    GenDefineTypes(lm, brackets: true);

                    _builder.Append(@$"({level}, new global::Microsoft.Extensions.Logging.EventId({lm.EventId}, {eventName}), ""{ConvertEndOfLineAndQuotationCharactersToEscapeForm(lm.Message)}"", new global::Microsoft.Extensions.Logging.LogDefineOptions() {{ SkipEnabledCheck = true }}); 
");
                }

                _builder.Append($@"
        {nestedIndentation}[{s_generatedCodeAttribute}]
        {nestedIndentation}{lm.Modifiers} void {lm.Name}({extension}");

                GenParameters(lm);

                _builder.Append($@")
        {nestedIndentation}{{");

                string enabledCheckIndentation = lm.SkipEnabledCheck ? "" : "    ";
                if (!lm.SkipEnabledCheck)
                {
                    _builder.Append($@"
            {nestedIndentation}if ({logger}.IsEnabled({level}))
            {nestedIndentation}{{");
                }

                if (UseLoggerMessageDefine(lm))
                {
                    _builder.Append($@"
            {nestedIndentation}{enabledCheckIndentation}__{lm.UniqueName}Callback({logger}, ");

                    GenCallbackArguments(lm);

                    _builder.Append($"{exceptionArg});");
                }
                else
                {
                    _builder.Append($@"
            {nestedIndentation}{enabledCheckIndentation}{logger}.Log(
                {nestedIndentation}{enabledCheckIndentation}{level},
                {nestedIndentation}{enabledCheckIndentation}new global::Microsoft.Extensions.Logging.EventId({lm.EventId}, {eventName}),
                {nestedIndentation}{enabledCheckIndentation}");
                GenHolder(lm);
                _builder.Append($@",
                {nestedIndentation}{enabledCheckIndentation}{exceptionArg},
                {nestedIndentation}{enabledCheckIndentation}__{lm.UniqueName}Struct.Format);");
                }

                if (!lm.SkipEnabledCheck)
                {
                    _builder.Append($@"
            {nestedIndentation}}}");
                }

                _builder.Append($@"
        {nestedIndentation}}}");

                static string GetException(LoggerMethod lm)
                {
                    string exceptionArg = "null";
                    foreach (LoggerParameter p in lm.AllParameters)
                    {
                        if (p.IsException)
                        {
                            exceptionArg = p.Name;
                            break;
                        }
                    }
                    return exceptionArg;
                }

                static string GetLogger(LoggerMethod lm)
                {
                    string logger = lm.LoggerField;
                    foreach (LoggerParameter p in lm.AllParameters)
                    {
                        if (p.IsLogger)
                        {
                            logger = p.Name;
                            break;
                        }
                    }
                    return logger;
                }

                static string GetLogLevel(LoggerMethod lm)
                {
                    string level = string.Empty;

                    if (lm.Level == null)
                    {
                        foreach (LoggerParameter p in lm.AllParameters)
                        {
                            if (p.IsLogLevel)
                            {
                                level = p.Name;
                                break;
                            }
                        }
                    }
                    else
                    {
                        level = lm.Level switch
                        {
                            0 => "global::Microsoft.Extensions.Logging.LogLevel.Trace",
                            1 => "global::Microsoft.Extensions.Logging.LogLevel.Debug",
                            2 => "global::Microsoft.Extensions.Logging.LogLevel.Information",
                            3 => "global::Microsoft.Extensions.Logging.LogLevel.Warning",
                            4 => "global::Microsoft.Extensions.Logging.LogLevel.Error",
                            5 => "global::Microsoft.Extensions.Logging.LogLevel.Critical",
                            6 => "global::Microsoft.Extensions.Logging.LogLevel.None",
                            _ => $"(global::Microsoft.Extensions.Logging.LogLevel){lm.Level}",
                        };
                    }

                    return level;
                }
            }

            private void GenEnumerationHelper()
            {
                if (_needEnumerationHelper)
                {
                                _builder.Append($@"
/// {GeneratedTypeSummary}
[{s_generatedCodeAttribute}]
[{EditorBrowsableAttribute}]
internal static class __LoggerMessageGenerator
{{
    public static string Enumerate(global::System.Collections.IEnumerable? enumerable)
    {{
        if (enumerable == null)
        {{
            return ""(null)"";
        }}

        var sb = new global::System.Text.StringBuilder();
        _ = sb.Append('[');

        bool first = true;
        foreach (object e in enumerable)
        {{
            if (!first)
            {{
                _ = sb.Append("", "");
            }}

            if (e == null)
            {{
                _ = sb.Append(""(null)"");
            }}
            else
            {{
                if (e is global::System.IFormattable fmt)
                {{
                    _ = sb.Append(fmt.ToString(null, global::System.Globalization.CultureInfo.InvariantCulture));
                }}
                else
                {{
                    _ = sb.Append(e);
                }}
            }}

            first = false;
        }}

        _ = sb.Append(']');

        return sb.ToString();
    }}
}}");
                }
            }
        }

        private static string ConvertEndOfLineAndQuotationCharactersToEscapeForm(string s)
        {
            int index = 0;
            while (index < s.Length)
            {
                if (s[index] is '\n' or '\r' or '"')
                {
                    break;
                }
                index++;
            }

            if (index >= s.Length)
            {
                return s;
            }

            StringBuilder sb = new StringBuilder(s.Length);
            sb.Append(s, 0, index);

            while (index < s.Length)
            {
                switch (s[index])
                {
                    case '\n':
                        sb.Append('\\');
                        sb.Append('n');
                        break;

                    case '\r':
                        sb.Append('\\');
                        sb.Append('r');
                        break;

                    case '"':
                        sb.Append('\\');
                        sb.Append('"');
                        break;

                    default:
                        sb.Append(s[index]);
                        break;
                }

                index++;
            }

            return sb.ToString();
        }
        /// <summary>
        /// Checks if variableOrTemplateName contains a special symbol ('@') as starting char
        /// </summary>
        /// <param name="variableOrTemplateName">variable that might contain '@' symbol</param>
        /// <returns>true if contains special symbol, false otherwise.</returns>
        private static bool ContainsSpecialSymbol(ReadOnlySpan<char> variableOrTemplateName)
            => variableOrTemplateName.Length > 0 && variableOrTemplateName[0] == '@';

        /// <summary>
        /// prefix the symbol name with _ if the symbol don't start with @, allowing local variables to be declared when creating code.
        /// </summary>
        /// <param name="variableOrTemplateName">variable that might contain '@' symbol</param>
        /// <returns>current variableName value if variableOrTemplateName if it does not starts with symbol ('@'), otherwise returns a new string with its first char '@' replaced by '_'.</returns>
        /// <remarks>This code only handles starting symbols. Symbols inside string will be kept.</remarks>
        private static string NormalizeSpecialSymbol(string variableOrTemplateName) =>
            ContainsSpecialSymbol(variableOrTemplateName.AsSpan()) ? variableOrTemplateName : $"_{variableOrTemplateName}";

        /// <summary>
        /// Remove leading symbol from variableOrTemplateName.
        /// </summary>
        /// <param name="variableOrTemplateName">String that might contains special symbol.</param>
        /// <returns>current variableOrTemplateName value if it does not contains starting '@' symbol, otherwise returns a new string with first char '@' removed.</returns>
        /// <remarks>This code only handles starting symbols. Symbols inside string will be kept.</remarks>
        private static ReadOnlySpan<char> RemoveSpecialSymbol(ReadOnlySpan<char> variableOrTemplateName)
            => ContainsSpecialSymbol(variableOrTemplateName) ? variableOrTemplateName.Slice(1) : variableOrTemplateName;
    }
}
